package lohr.lang.dom;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * An API for processing a lohr DOM document based on the visitor pattern.
 * 
 * The visit methods return a boolean that indicates if children should be processed.
 * 
 * The complete methods are invoked after invoking the visit method and visiting 
 * all children elements.
 * 
 */
public class AbstractDOMVisitor implements DOMVisitor {
	
	
	private static final Map<Class<?>, Map<Class<?>, Method>> __visitMethodCache= new HashMap<Class<?>, Map<Class<?>,Method>>();
	private static final Map<Class<?>, Map<Class<?>, Method>> __leaveMethodCache= new HashMap<Class<?>, Map<Class<?>,Method>>();
	private static final Method __defaultVisitMethod;
	private static final Method __defaultLeaveMethod;
	static {
		try {
			__defaultVisitMethod= DOMVisitor.class.getMethod("visit", LohrElement.class );
			__defaultLeaveMethod= DOMVisitor.class.getMethod("complete", LohrElement.class );
		} catch (Exception e) {
			throw new RuntimeException("Internal Error", e);
		}
	}

	
	/**
	 * Find the appropriate visit method for the given node and visitor.
	 * Use annotations find visit method dynamically based on node id.
	 * visit method will be named "visit"+id
	 * 
	 */
	public static Method findVisitMethod(DOMVisitor visitor, LohrElement node) {
		

		Method visitMethod= null;

		Class<?> visitorClass= visitor.getClass();
		Map<Class<?>, Method> visitMethods= __visitMethodCache.get(visitorClass);
		if (visitMethods == null) 
			__visitMethodCache.put(visitorClass, visitMethods= new HashMap<Class<?>, Method>());
		visitMethod= visitMethods.get(visitorClass);
		
		if (visitMethod == null) {
			try {
				visitMethod= visitorClass.getMethod("visit", visitorClass);
				visitMethods.put(visitorClass, visitMethod);
			} catch (Exception e) {
			}
		}

		if (visitMethod == null) 
			return __defaultVisitMethod;

		return visitMethod;
	}

	
	/**
	 * Find the appropriate leave method for the given node and visitor.
	 */
	public static Method findLeaveMethod(DOMVisitor visitor, LohrElement node) {

		Method visitMethod= null;

		Class<?> visitorClass= visitor.getClass();
		Map<Class<?>, Method> visitMethods= __leaveMethodCache.get(visitorClass);
		if (visitMethods == null) 
			__leaveMethodCache.put(visitorClass, visitMethods= new HashMap<Class<?>, Method>());
		visitMethod= visitMethods.get(visitorClass);
		
		if (visitMethod == null) {
			try {
				visitMethod= visitorClass.getMethod("complete", visitorClass);
				visitMethods.put(visitorClass, visitMethod);
			} catch (Exception e) {
			}
		}

		if (visitMethod == null) 
			return __defaultLeaveMethod;

		return visitMethod;
	}
	


	private HashMap<LohrElement, Object> _results= new HashMap<LohrElement, Object>();
	private HashSet<LohrElement> _visited= new HashSet<LohrElement>();
	private HashSet<LohrElement> _completed= new HashSet<LohrElement>();
	private LohrElement _currentMatch;
	
	@Override
	public boolean visit(LohrElement node) {

		if (_visited.contains(node))
			return true;
		_visited.add(node);
		LohrElement prevMatch= _currentMatch;
		_currentMatch= node;
		Method visitMethod = findVisitMethod(this, node);
		try {
			Boolean visitChildren= (Boolean) visitMethod.invoke(this, new Object[] { node });
			return visitChildren;
			
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Error invoking method '"+visitMethod.getName()+"'", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Error invoking method '"+visitMethod.getName()+"'", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("Error invoking method '"+visitMethod.getName()+"'", e);
		}
		finally {
			_currentMatch= prevMatch;
		}
	}
	
	/**
	 * Invoked after invoking the visit method and visiting all children nodes   
	 */
	@Override
	public void complete(LohrElement node) {
		if (_completed.contains(node))
			return;
		_completed.add(node);

		LohrElement prevMatch= _currentMatch;
		_currentMatch= node;
		Method leaveMethod = findLeaveMethod(this, node);
		try {
			leaveMethod.invoke(this, new Object[] { node });
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Error invoking method '"+leaveMethod.getName()+"'", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Error invoking method '"+leaveMethod.getName()+"'", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("Error invoking method '"+leaveMethod.getName()+"'", e);
		}
		finally {
			_currentMatch= prevMatch;
		}
	}
	
	public LohrElement getCurrentMatch() {
		return _currentMatch;
	}

	/**
	 * Save result for the Match that is currently being processed 
	 */
	public void putResult(Object result) {
		putResult(getCurrentMatch(), result);
		
	}

	public void putResult(LohrElement match, Object result) {
		_results.put(match, result);
		
	}

	/**
	 * Get the result for the given Match.
	 * If there is no result and the Match has not yet been processed 
	 * then the Match will be processed before returning. 
	 */
	@SuppressWarnings("unchecked")
	public <T> T getResult(LohrElement match) {
		if (match == null)
			return null;
		Object result= _results.get(match);
		if (result == null && !_visited.contains(match)) {
			match.accept(this);
			result= _results.get(match);
		}
		return (T) result;
	}

}
